package mx.com.crowdgine.generator;

import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;

import javax.imageio.ImageIO;

import mx.com.crowdgine.entity.Entity;
import mx.com.crowdgine.entity.geometry.Plane;
import mx.com.crowdgine.util.lsystem.LSystem;
import mx.com.crowdgine.util.math.MathCommons;
import mx.com.crowdgine.util.math.Vector2f;
import mx.com.crowdgine.util.math.Vector3f;
import mx.com.crowdgine.util.xml.XMLReader;

import org.w3c.dom.Node;

public class TerrainGenerator {
	
	private final String mapFileName = "Assets/Textures/map.jpg";

	private boolean verbose = true;
	private int terrainWidth = 0;
	private int terrainDepth = 0;
	private int terrainHeight = 0;
	
	private int terrainType = 0;
	private int terrainScale = 1;
	
	Plane ground;
	
	LinkedList<Entity> terrainEntities;
	
	BufferedImage mapImage;
	float roadMapArray[];
	
	public TerrainGenerator(Node terrain, boolean... verbose){
		if(verbose.length > 0)
			this.verbose = verbose[0];
		
		terrainEntities = new LinkedList<Entity>();
		
		LinkedList<HashMap<String, String>> terrainInfo = XMLReader.getNodeInformation(terrain);
		
		String type = terrainInfo.get(0).get(XMLReader.TERRAIN_TYPE);
		if(type != null){
			if(type.equals("city"))
				terrainType = 1;
			else if(type.equals("nature"))
				terrainType = 2;
			else if(type.equals("mall"))
				terrainType = 3;
			else
				terrainType = 1;
		}else{
			terrainType = 1;
		}
		
		String scale = terrainInfo.get(0).get(XMLReader.TERRAIN_SCALE);
		if(scale != null){
			terrainScale = Integer.parseInt(scale);
		}else{
			terrainScale = 1;
		}
		
		String sizeX = terrainInfo.get(0).get(XMLReader.TERRAIN_SIZE_X);
		String sizeZ = terrainInfo.get(0).get(XMLReader.TERRAIN_SIZE_Z);
		if(sizeX != null || sizeZ != null){
			terrainWidth = Integer.parseInt(sizeX);
			terrainDepth = Integer.parseInt(sizeZ);

			/*if(terrainWidth < 1024)
				terrainWidth = 1024;
			
			if(terrainDepth < 800)
				terrainDepth = 800;*/
			
			String sizeY = terrainInfo.get(0).get(XMLReader.TERRAIN_SIZE_Y);
			if(sizeZ != null)
				terrainHeight = Integer.parseInt(sizeY);
			else
				terrainHeight = 1;
			
			if(terrainHeight > 4)
				terrainHeight = 1;
		}else{
			terrainWidth = 1024;
			terrainDepth = 1024;
			terrainHeight = 1;
		}
		
		if(this.verbose)
			System.out.println("Terrain Dimensions: (" + terrainWidth + ", " + terrainDepth + ", " + terrainHeight + ")");
		
		//Get the tags inside the terrain node
		HashMap<String, Node> terrainHash = XMLReader.buildHashMap(terrain);
		
		//Init Terrain properties
		loadAssets(terrainHash);
		buildGround(terrainHash);
	}
	
	private void loadAssets(HashMap<String, Node> terrainHash){
		
	}
	
	private void buildGround(HashMap<String, Node> terrainHash){
		if(terrainHeight == 1)
			ground = new Plane(new Vector3f(terrainWidth*terrainScale, 0, 0), new Vector3f(0, 0, terrainDepth*terrainScale), new Vector3f(0, 0, 0));
		
		terrainEntities.add(ground);
		
		switch(terrainType){
			case 1:
				Node road = null;
				if(terrainHash != null)
					road = terrainHash.get(XMLReader.TERRAIN_ROAD);
				generateCity(road);
				break;
			case 2:
				generateTrees();
				break;
			case 3:
				generateStore();
				break;
		}
	}
	
	private void generateCity(Node road){
		generateRoadMap(road);
	}
	
	private boolean generateRoadMap(Node road){
		int borderSize = 0;
		int roadType = 0;
		
		//
		if(road != null){
			LinkedList<HashMap<String, String>> roadInfo = XMLReader.getNodeInformation(road);
			for(int i=0; i<roadInfo.size(); i++){
				HashMap<String, String> element = roadInfo.get(i);
				
				if(element.containsKey("imageBorder")){
					borderSize = Integer.parseInt(element.get("imageBorder"));
					if(borderSize >= Math.min(terrainWidth, terrainDepth))
						borderSize = 0;
				}
				
				if(element.containsKey("roadType")){
					roadType = Integer.parseInt(element.get("roadType"));
					if(roadType > 6 || roadType < 0)
						roadType = 0;
				}

			}
		}
		
		//Image buffer and file
		mapImage = new BufferedImage(terrainWidth, terrainDepth, BufferedImage.TYPE_INT_RGB);
		Graphics2D gc = mapImage.createGraphics();
		File mapFile = new File(mapFileName);
		LinkedList<Vector2f> streetPoints = new LinkedList<Vector2f>();
		
		if(roadType != 0){
			//LSystem for road generation
			LSystem roadMapGenerator;
			double angle = 0;
			switch(roadType){
				case 1:
					roadMapGenerator = new LSystem(LSystem.DRAGON_CURVE);
					angle = LSystem.DRAGON_ANGLE;
					roadMapGenerator.iterate(10);
					break;
				case 2:
					roadMapGenerator = new LSystem(LSystem.SIERPINSKI_TRIANGLE);
					angle = LSystem.SIERPINSKI_ANGLE;
					roadMapGenerator.iterate(8);
					break;
				case 3:
					roadMapGenerator = new LSystem(LSystem.KOCH_CURVE);
					angle = LSystem.KOCH_ANGLE;
					roadMapGenerator.iterate(4);
					break;
				case 4:
					roadMapGenerator = new LSystem(LSystem.ORIAM_CURVE);
					angle = LSystem.ORIAM_ANGLE;
					roadMapGenerator.iterate(8);
					break;
				case 5:
					roadMapGenerator = new LSystem();
					angle = LSystem.SIERPINSKI_ANGLE;
					roadMapGenerator.iterate(6);
					break;
				default:
					roadMapGenerator = new LSystem();
					angle = LSystem.SIERPINSKI_ANGLE;
					roadMapGenerator.iterate(6);
					break;
			}

			String roadMap = roadMapGenerator.getTree();
			int length = roadMap.length();
		
			Vector3f position = new Vector3f(0, 6, 0);
			Vector3f direction = new Vector3f(1, 0, 0);
			//Vector3f buildingColor = new Vector3f(1f, 0.72f, 0.05f);
			
			float coordX, coordZ;
			float bbMinX = 0, bbMinZ = 0, bbMaxX = 0, bbMaxZ = 0;
			
			//Cube building;
			//int totalBuildings = 0;
			//building = new Cube(10, 12, 5, position, buildingColor);
			//terrainEntities.add(building);
			streetPoints.add(new Vector2f(position.getX(), position.getZ()));
			//totalBuildings++;
			
			for (int i=0; i<length; i++) {
				switch (roadMap.charAt(i)) {
					case 'F':
					case 'A':
					case 'B':
						position.plus(direction.mul(30));
						streetPoints.add(new Vector2f(position.getX(), position.getZ()));
						float tempX = position.getX();
						float tempZ = position.getZ();
						if(tempX < bbMinX)
							bbMinX = tempX;
						else if(tempX > bbMaxX)
							bbMaxX = tempX;
						if(tempZ < bbMinZ)
							bbMinZ = tempZ;
						else if(tempZ > bbMaxZ)
							bbMaxZ = tempZ;
						//building = new Cube(10, 12, 5, position, buildingColor);
						//terrainEntities.add(building);
						//totalBuildings++;
						break;
					case '+':
						coordX = (float) ((direction.getX() * Math.cos(angle)) - (direction.getZ() * Math.sin(angle)));
						coordZ = (float) ((direction.getZ() * Math.cos(angle)) + (direction.getX() * Math.sin(angle)));
						MathCommons.clamp(coordX, -1, 1);
						MathCommons.clamp(coordZ, -1, 1);
						direction.setX(coordX);
						direction.setZ(coordZ);
						break;
					case '-':
						coordX = (float) ((direction.getX() * Math.cos(-angle)) - (direction.getZ() * Math.sin(-angle)));
						coordZ = (float) ((direction.getZ() * Math.cos(-angle)) + (direction.getX() * Math.sin(-angle)));
						MathCommons.clamp(coordX, -1, 1);
						MathCommons.clamp(coordZ, -1, 1);
						direction.setX(coordX);
						direction.setZ(coordZ);
						break;
					default:
				}
			}
			
			float bbLengthX = (float)Math.ceil(bbMaxX - bbMinX);
			float bbLengthZ = (float)Math.ceil(bbMaxZ - bbMinZ);
	
			float scaleValueX = (terrainWidth - borderSize) / bbLengthX;
			float scaleValueZ = (terrainDepth - borderSize) / bbLengthZ;
			
			float halfWidth = terrainWidth / 2;
			float halfDepth = terrainDepth / 2;
			
			float scaledBBMinX = bbMinX * scaleValueX;
			float scaledBBMaxX = bbMaxX * scaleValueX;
			float scaledBBMinZ = bbMinZ * scaleValueZ;
			float scaledBBMaxZ = bbMaxZ * scaleValueZ;
			
			float bbCenterX = -1.0f * (float)Math.ceil(scaledBBMaxX + scaledBBMinX)/2;
			float bbCenterZ = -1.0f * (float)Math.ceil(scaledBBMaxZ + scaledBBMinZ)/2;
			
			gc.setStroke(new BasicStroke(7));
			for(int i=1; i<streetPoints.size(); i++){
				int originX = (int)(streetPoints.get(i-1).getX() * scaleValueX + bbCenterX + halfWidth);
				int originZ = (int)(streetPoints.get(i-1).getY() * scaleValueZ + bbCenterZ + halfDepth);
				int lengthX = (int)(streetPoints.get(i).getX() * scaleValueX + bbCenterX + halfWidth);
				int lengthZ = (int)(streetPoints.get(i).getY() * scaleValueZ + bbCenterZ + halfDepth);
				gc.drawLine((int)originX, (int)originZ, lengthX, lengthZ);
			}
			
		}else{
			gc.fillRect(borderSize, borderSize, terrainWidth-2*borderSize, terrainDepth-2*borderSize);
		}
		
		try {
			ImageIO.write(mapImage, "jpg", mapFile);
		} catch (IOException e) {
			System.out.println("Error saving terrain image, using default road map");
			return false;
		}
		createRoadMapArray();
		return true;
	}
	
	private void generateStore(){
		
	}
	
	private void generateTrees(){
		
	}
	
	public LinkedList<Entity> getEntities(){
		return terrainEntities;
	}
	
	public String getMapFileName(){
		return mapFileName;
	}
	
	private void createRoadMapArray(){
		int width = mapImage.getWidth();
		int height = mapImage.getHeight();
		roadMapArray = new float[4 * width * height];
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				int pixel = mapImage.getRGB(j, i);
		        float alpha = ((pixel >> 24) & 0xff) / 255;
		        float red = ((pixel >> 16) & 0xff) / 255;
		        float green = ((pixel >> 8) & 0xff) / 255;
		        float blue = ((pixel) & 0xff) / 255;
		        roadMapArray[(4 * (i*width + j)) + 0] = red;
		        roadMapArray[(4 * (i*width + j)) + 1] = green;
		        roadMapArray[(4 * (i*width + j)) + 2] = blue;
		        roadMapArray[(4 * (i*width + j)) + 3] = alpha;
			}
		}
	}
	
	public float[] getRoadMapData(){
		return roadMapArray;
	}
	
	public int getRoadMapWidth(){
		return terrainWidth;
	}
	
	public int getRoadMapHeight(){
		return terrainDepth;
	}
	
	public int getRoadMapScale(){
		return terrainScale;
	}
}
